// 
//   SubSonic - http://subsonicproject.com
// 
//   The contents of this file are subject to the New BSD
//   License (the "License"); you may not use this file
//   except in compliance with the License. You may obtain a copy of
//   the License at http://www.opensource.org/licenses/bsd-license.php
//  
//   Software distributed under the License is distributed on an 
//   "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
//   implied. See the License for the specific language governing
//   rights and limitations under the License.
// 
using System;

namespace MVCPets.Extensions
{
  /// <summary>
  /// Summary for the Dates class
  /// </summary>
  public static class Dates
  {
    public const string AGO = "ago";
    public const string DAY = "day";
    public const string HOUR = "hour";
    public const string MINUTE = "minute";
    public const string MONTH = "month";
    public const string SECOND = "second";
    public const string SPACE = " ";
    public const string YEAR = "year";

    #region Methods to deal with Courseware NDate
    public static DateTime FromCoursewareNDate(this double ndate)
    {
      // Courseware has an off by one error :(
      return DateTime.FromOADate(ndate + 1);
    }

    public static DateTime FromCoursewareNDate(this int ndate)
    {
      // Courseware has an off by one error :(
      return ((double)ndate).FromCoursewareNDate();
    }

    public static double ToCoursewareNDate(this DateTime date)
    {
      // Courseware has an off by one error :(
      return date.ToOADate() - 1;
    }
    #endregion

    #region Tomorrow
    public static DateTime Tomorrow(this DateTime value)
    {
      return value.AddDays(1);
    }
    #endregion

    #region Yesterday
    public static DateTime Yesterday(this DateTime value)
    {
      return value.AddDays(-1);
    }
    #endregion

    #region Date Math

    /// <summary>
    /// Returns a date in the past by days.
    /// </summary>
    /// <param name="days">The days.</param>
    /// <returns></returns>
    public static DateTime DaysAgo(this int days)
    {
      TimeSpan t = new TimeSpan(days, 0, 0, 0);
      return DateTime.Now.Subtract(t);
    }

    /// <summary>
    ///  Returns a date in the future by days.
    /// </summary>
    /// <param name="days">The days.</param>
    /// <returns></returns>
    public static DateTime DaysFromNow(this int days)
    {
      TimeSpan t = new TimeSpan(days, 0, 0, 0);
      return DateTime.Now.Add(t);
    }

    /// <summary>
    /// Returns a date in the past by hours.
    /// </summary>
    /// <param name="hours">The hours.</param>
    /// <returns></returns>
    public static DateTime HoursAgo(this int hours)
    {
      TimeSpan t = new TimeSpan(hours, 0, 0);
      return DateTime.Now.Subtract(t);
    }

    /// <summary>
    /// Returns a date in the future by hours.
    /// </summary>
    /// <param name="hours">The hours.</param>
    /// <returns></returns>
    public static DateTime HoursFromNow(this int hours)
    {
      TimeSpan t = new TimeSpan(hours, 0, 0);
      return DateTime.Now.Add(t);
    }

    /// <summary>
    /// Returns a date in the past by minutes
    /// </summary>
    /// <param name="minutes">The minutes.</param>
    /// <returns></returns>
    public static DateTime MinutesAgo(this int minutes)
    {
      TimeSpan t = new TimeSpan(0, minutes, 0);
      return DateTime.Now.Subtract(t);
    }

    /// <summary>
    /// Returns a date in the future by minutes.
    /// </summary>
    /// <param name="minutes">The minutes.</param>
    /// <returns></returns>
    public static DateTime MinutesFromNow(this int minutes)
    {
      TimeSpan t = new TimeSpan(0, minutes, 0);
      return DateTime.Now.Add(t);
    }

    /// <summary>
    /// Gets a date in the past according to seconds
    /// </summary>
    /// <param name="seconds">The seconds.</param>
    /// <returns></returns>
    public static DateTime SecondsAgo(this int seconds)
    {
      TimeSpan t = new TimeSpan(0, 0, seconds);
      return DateTime.Now.Subtract(t);
    }

    /// <summary>
    /// Gets a date in the future by seconds.
    /// </summary>
    /// <param name="seconds">The seconds.</param>
    /// <returns></returns>
    public static DateTime SecondsFromNow(this int seconds)
    {
      TimeSpan t = new TimeSpan(0, 0, seconds);
      return DateTime.Now.Add(t);
    }

    #endregion

    #region Diffs

    /// <summary>
    /// Diffs the specified date.
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static TimeSpan Diff(this DateTime dateOne, DateTime dateTwo)
    {
      TimeSpan t = dateOne.Subtract(dateTwo);
      return t;
    }

    /// <summary>
    /// Returns a double indicating the number of days between two dates (past is negative)
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static double DiffDays(this string dateOne, string dateTwo)
    {
      DateTime dtOne;
      DateTime dtTwo;
      if (DateTime.TryParse(dateOne, out dtOne) && DateTime.TryParse(dateTwo, out dtTwo))
        return Diff(dtOne, dtTwo).TotalDays;
      return 0;
    }

    /// <summary>
    /// Returns a double indicating the number of days between two dates (past is negative)
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static double DiffDays(this DateTime dateOne, DateTime dateTwo)
    {
      return Diff(dateOne, dateTwo).TotalDays;
    }

    /// <summary>
    /// Returns a double indicating the number of days between two dates (past is negative)
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static double DiffHours(this string dateOne, string dateTwo)
    {
      DateTime dtOne;
      DateTime dtTwo;
      if (DateTime.TryParse(dateOne, out dtOne) && DateTime.TryParse(dateTwo, out dtTwo))
        return Diff(dtOne, dtTwo).TotalHours;
      return 0;
    }

    /// <summary>
    /// Returns a double indicating the number of days between two dates (past is negative)
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static double DiffHours(this DateTime dateOne, DateTime dateTwo)
    {
      return Diff(dateOne, dateTwo).TotalHours;
    }

    /// <summary>
    /// Returns a double indicating the number of days between two dates (past is negative)
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static double DiffMinutes(this string dateOne, string dateTwo)
    {
      DateTime dtOne;
      DateTime dtTwo;
      if (DateTime.TryParse(dateOne, out dtOne) && DateTime.TryParse(dateTwo, out dtTwo))
        return Diff(dtOne, dtTwo).TotalMinutes;
      return 0;
    }

    /// <summary>
    /// Returns a double indicating the number of days between two dates (past is negative)
    /// </summary>
    /// <param name="dateOne">The date one.</param>
    /// <param name="dateTwo">The date two.</param>
    /// <returns></returns>
    public static double DiffMinutes(this DateTime dateOne, DateTime dateTwo)
    {
      return Diff(dateOne, dateTwo).TotalMinutes;
    }

    /// <summary>
    /// Displays the difference in time between the two dates. Return example is "12 years 4 months 24 days 8 hours 33 minutes 5 seconds"
    /// </summary>
    /// <param name="startTime">The start time.</param>
    /// <param name="endTime">The end time.</param>
    /// <returns></returns>
    public static string ReadableDiff(this DateTime startTime, DateTime endTime)
    {
      string result;

      int seconds = endTime.Second - startTime.Second;
      int minutes = endTime.Minute - startTime.Minute;
      int hours = endTime.Hour - startTime.Hour;
      int days = endTime.Day - startTime.Day;
      int months = endTime.Month - startTime.Month;
      int years = endTime.Year - startTime.Year;

      if (seconds < 0)
      {
        minutes--;
        seconds += 60;
      }
      if (minutes < 0)
      {
        hours--;
        minutes += 60;
      }
      if (hours < 0)
      {
        days--;
        hours += 24;
      }

      if (days < 0)
      {
        months--;
        int previousMonth = (endTime.Month == 1) ? 12 : endTime.Month - 1;
        int year = (previousMonth == 12) ? endTime.Year - 1 : endTime.Year;
        days += DateTime.DaysInMonth(year, previousMonth);
      }
      if (months < 0)
      {
        years--;
        months += 12;
      }

      //put this in a readable format
      if (years > 0)
      {
        result = years.Pluralize(YEAR);
        if (months != 0)
          result += ", " + months.Pluralize(MONTH);
        result += " ago";
      }
      else if (months > 0)
      {
        result = months.Pluralize(MONTH);
        if (days != 0)
          result += ", " + days.Pluralize(DAY);
        result += " ago";
      }
      else if (days > 0)
      {
        result = days.Pluralize(DAY);
        if (hours != 0)
          result += ", " + hours.Pluralize(HOUR);
        result += " ago";
      }
      else if (hours > 0)
      {
        result = hours.Pluralize(HOUR);
        if (minutes != 0)
          result += ", " + minutes.Pluralize(MINUTE);
        result += " ago";
      }
      else if (minutes > 0)
        result = minutes.Pluralize(MINUTE) + " ago";
      else
        result = seconds.Pluralize(SECOND) + " ago";

      return result;
    }

    #endregion

    #region Between
    /// <summary>
    /// Determines if the date is between 2 dates
    /// </summary>
    /// <param name="value">The original date</param>
    /// <param name="lower">The lower boundary</param>
    /// <param name="higher">The higher boundary</param>
    /// <param name="options">Comparison options</param>
    /// <returns>bool result</returns>
    public static bool IsBetween(this DateTime value, DateTime lower, DateTime higher, BetweenOptions options)
    {
      bool ret = false;

      if (options == BetweenOptions.BetweenOnly) ret = value > lower && value < higher;
      else if (options == BetweenOptions.IncludeBoth) ret = value >= lower && value <= higher;
      else if (options == BetweenOptions.IncludeHigher) ret = value > lower && value <= higher;
      else if (options == BetweenOptions.IncludeLower) ret = value >= lower && value < higher;

      return ret;
    }

    /// <summary>
    /// Determines if the date is between 2 dates
    /// </summary>
    /// <param name="value">The original date</param>
    /// <param name="lower">The lower boundary</param>
    /// <param name="higher">The higher boundary</param>
    /// <param name="options">Comparison options</param>
    /// <returns>bool result</returns>
    public static bool IsBetween(this DateTime value, DateTime lower, DateTime higher)
    {
      return IsBetween(value, lower, higher, BetweenOptions.BetweenOnly);
    }
    #endregion

    #region Fiscal Year
    /// <summary>
    /// Returns the fiscal year for the passed in date
    /// </summary>
    /// <param name="value">the date</param>
    /// <returns>the fiscal year</returns>
    public static int FiscalYear(this DateTime value)
    {
      int ret = value.Year;

      if (value.Month >= 7) ret++;

      return ret;
    }

    /// <summary>
    /// Returns the fiscal year for the passed in date
    /// </summary>
    /// <param name="value">the date</param>
    /// <returns>the fiscal year</returns>
    public static string FiscalYearString(this DateTime value)
    {
      int fy = FiscalYear(value);

      return "{0}/{1}".FormatString(fy - 1, fy);
    }
    #endregion

    /// <summary>
    /// Gets a DateTime representing the first day in the current month
    /// </summary>
    /// <param name="current">The current date</param>
    /// <returns></returns>
    public static DateTime First(this DateTime current)
    {
      DateTime first = current.AddDays(1 - current.Day);
      return first;
    }

    /// <summary>
    /// Gets a DateTime representing the first specified day in the current month
    /// </summary>
    /// <param name="current">The current day</param>
    /// <param name="dayOfWeek">The current day of week</param>
    /// <returns></returns>
    public static DateTime First(this DateTime current, DayOfWeek dayOfWeek)
    {
      DateTime first = current.First();

      if (first.DayOfWeek != dayOfWeek)
      {
        first = first.Next(dayOfWeek);
      }

      return first;
    }

    /// <summary>
    /// Gets a DateTime representing the last day in the current month
    /// </summary>
    /// <param name="current">The current date</param>
    /// <returns></returns>
    public static DateTime Last(this DateTime current)
    {
      int daysInMonth = DateTime.DaysInMonth(current.Year, current.Month);

      DateTime last = current.First().AddDays(daysInMonth - 1);
      return last;
    }

    /// <summary>
    /// Gets a DateTime representing the last specified day in the current month
    /// </summary>
    /// <param name="current">The current date</param>
    /// <param name="dayOfWeek">The current day of week</param>
    /// <returns></returns>
    public static DateTime Last(this DateTime current, DayOfWeek dayOfWeek)
    {
      DateTime last = current.Last();

      last = last.AddDays(Math.Abs(dayOfWeek - last.DayOfWeek) * -1);
      return last;
    }

    /// <summary>
    /// Gets the previous date for the past in day of the week.
    /// </summary>
    /// <param name="value"></param>
    /// <param name="dayOfWeek"></param>
    /// <returns></returns>
    public static DateTime Previous(this DateTime value, DayOfWeek dayOfWeek)
    {
      do { value = value.AddDays(-1); } while (value.DayOfWeek != dayOfWeek);

      return value;
    }

    /// <summary>
    /// Gets a DateTime representing the first date following the current date which falls on the given day of the week
    /// </summary>
    /// <param name="current">The current date</param>
    /// <param name="dayOfWeek">The day of week for the next date to get</param>
    public static DateTime Next(this DateTime current, DayOfWeek dayOfWeek)
    {
      int offsetDays = dayOfWeek - current.DayOfWeek;

      if (offsetDays <= 0)
      {
        offsetDays += 7;
      }

      DateTime result = current.AddDays(offsetDays);
      return result;
    }

    // many thanks to ASP Alliance for the code below
    // http://authors.aspalliance.com/olson/methods/
    /// <summary>
    /// Counts the number of weekdays between two dates.
    /// </summary>
    /// <param name="startTime">The start time.</param>
    /// <param name="endTime">The end time.</param>
    /// <returns></returns>
    public static int CountWeekdays(this DateTime startTime, DateTime endTime)
    {
      TimeSpan ts = endTime - startTime;
      Console.WriteLine(ts.Days);
      int cnt = 0;
      for (int i = 0; i < ts.Days; i++)
      {
        DateTime dt = startTime.AddDays(i);
        if (IsWeekDay(dt))
          cnt++;
      }
      return cnt;
    }

    /// <summary>
    /// Counts the number of weekends between two dates.
    /// </summary>
    /// <param name="startTime">The start time.</param>
    /// <param name="endTime">The end time.</param>
    /// <returns></returns>
    public static int CountWeekends(this DateTime startTime, DateTime endTime)
    {
      TimeSpan ts = endTime - startTime;
      Console.WriteLine(ts.Days);
      int cnt = 0;
      for (int i = 0; i < ts.Days; i++)
      {
        DateTime dt = startTime.AddDays(i);
        if (IsWeekEnd(dt))
          cnt++;
      }
      return cnt;
    }    

    /// <summary>
    /// Checks to see if the date is a week day (Mon - Fri)
    /// </summary>
    /// <param name="dt">The dt.</param>
    /// <returns>
    /// 	<c>true</c> if [is week day] [the specified dt]; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsWeekDay(this DateTime dt)
    {
      return (dt.DayOfWeek != DayOfWeek.Saturday && dt.DayOfWeek != DayOfWeek.Sunday);
    }

    /// <summary>
    /// Checks to see if the date is Saturday or Sunday
    /// </summary>
    /// <param name="dt">The dt.</param>
    /// <returns>
    /// 	<c>true</c> if [is week end] [the specified dt]; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsWeekEnd(this DateTime dt)
    {
      return (dt.DayOfWeek == DayOfWeek.Saturday || dt.DayOfWeek == DayOfWeek.Sunday);
    }

    /// <summary>
    /// Displays the difference in time between the two dates. Return example is "12 years 4 months 24 days 8 hours 33 minutes 5 seconds"
    /// </summary>
    /// <param name="startTime">The start time.</param>
    /// <param name="endTime">The end time.</param>
    /// <returns></returns>
    public static string TimeDiff(this DateTime startTime, DateTime endTime)
    {
      int seconds = endTime.Second - startTime.Second;
      int minutes = endTime.Minute - startTime.Minute;
      int hours = endTime.Hour - startTime.Hour;
      int days = endTime.Day - startTime.Day;
      int months = endTime.Month - startTime.Month;
      int years = endTime.Year - startTime.Year;
      if (seconds < 0)
      {
        minutes--;
        seconds += 60;
      }
      if (minutes < 0)
      {
        hours--;
        minutes += 60;
      }
      if (hours < 0)
      {
        days--;
        hours += 24;
      }
      if (days < 0)
      {
        months--;
        int previousMonth = (endTime.Month == 1) ? 12 : endTime.Month - 1;
        int year = (previousMonth == 12) ? endTime.Year - 1 : endTime.Year;
        days += DateTime.DaysInMonth(year, previousMonth);
      }
      if (months < 0)
      {
        years--;
        months += 12;
      }

      string sYears = FormatString(YEAR, String.Empty, years);
      string sMonths = FormatString(MONTH, sYears, months);
      string sDays = FormatString(DAY, sMonths, days);
      string sHours = FormatString(HOUR, sDays, hours);
      string sMinutes = FormatString(MINUTE, sHours, minutes);
      string sSeconds = FormatString(SECOND, sMinutes, seconds);

      return String.Concat(sYears, sMonths, sDays, sHours, sMinutes, sSeconds);
    }

    /// <summary>
    /// Given a datetime object, returns the formatted month and day, i.e. "April 15th"
    /// </summary>
    /// <param name="date">The date to extract the string from</param>
    /// <returns></returns>
    public static string GetFormattedMonthAndDay(this DateTime date)
    {
      return String.Concat(String.Format("{0:MMMM}", date), " ", GetDateDayWithSuffix(date));
    }

    /// <summary>
    /// Given a datetime object, returns the formatted day, "15th"
    /// </summary>
    /// <param name="date">The date to extract the string from</param>
    /// <returns></returns>
    public static string GetDateDayWithSuffix(this DateTime date)
    {
      int dayNumber = date.Day;
      string suffix = "th";

      if (dayNumber == 1 || dayNumber == 21 || dayNumber == 31)
        suffix = "st";
      else if (dayNumber == 2 || dayNumber == 22)
        suffix = "nd";
      else if (dayNumber == 3 || dayNumber == 23)
        suffix = "rd";

      return String.Concat(dayNumber, suffix);
    }

    /// <summary>
    /// Remove leading strings with zeros and adjust for singular/plural
    /// </summary>
    /// <param name="str">The STR.</param>
    /// <param name="previousStr">The previous STR.</param>
    /// <param name="t">The t.</param>
    /// <returns></returns>
    private static string FormatString(this string str, string previousStr, int t)
    {
      if ((t == 0) && (previousStr.Length == 0))
        return String.Empty;

      string suffix = (t == 1) ? String.Empty : "s";
      return String.Concat(t, SPACE, str, suffix, SPACE);
    }
  }
}